/**
 * 
 */
package net.sf.cldsql.dj.mvc;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.SQLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.sf.cldsql.dj.container.DiContainer;
import net.sf.cldsql.dj.container.base.DiAop;
import net.sf.cldsql.dj.db.ConnPool;
import net.sf.cldsql.dj.db.DbConnection;
import net.sf.cldsql.dj.utl.DiException;

/**
 * ConnTransAop
 * 
 * initConn, initTx and null state of aop context connection control cascade service call. Consider the following case.<br/> 
   1. method of service A is called <br/>
   2. method of service B is called in service A <br/>
   3. method of service C is called after service B in service A <br/>
     Three aop objects are created for method of service A, B, C corresponding to step 1, 2, 3.
     Only the outer side service method invokation, the step 1, have initConn and initTx been set,
     Others inner side service method invokation, the step 2, step 3, the initConn and initTx are remained false.
     So, only the aop of service A method invokation will commit or rollback transaction and release connection.
     All of connection operations in aop should check these two flags. 
 */
public class ConnTransAop implements DiAop<DiService> {
    
    private static final ThreadLocal<ConnTransAopContext> threadLocal = new ThreadLocal<ConnTransAopContext>();
    
    final Logger log = LoggerFactory.getLogger(ConnTransAop.class);
    
    private DiContainer container;

    /** true when connection is allocated by current aop, see also: the class document */
    private boolean initConn = false;

    /** true when transaction is opened by current aop, see also: the class document */
    private boolean initTx = false;
    
    private boolean releasedConn = false;
    
    private boolean endedTx = false;

    private int beforeCascadeCnt;

    private int beforeConnAllocCnt;

    private boolean connAlloced;

    private boolean beforeFinished;
    
    public void setContainer (DiContainer container) {
        this.container = container;
    }
    
    public class ConnTransAopContext {
        private DbConnection conn;
        private int cascade = 0;
        private int connAllocCnt = 0;
        public DbConnection getConn() {
            return conn;
        }
        public void setConn(DbConnection conn) {
            this.conn = conn;
        }
        public int getCascade() {
            return cascade;
        }
        public int increaseCascade() {
            return ++cascade;
        }
        public int decreaseCascade() {
            return --cascade;
        }
        public int getConnAllocCnt() {
            return connAllocCnt ;
        }
        public int increaseConnAllocCnt() {
            return ++connAllocCnt;
        }
        public int decreaseConnAllocCnt() {
            return --connAllocCnt;
        }
    }
    
    public DiService afterCreate(DiService delegate, Constructor constructor, Object[] args, DiService proxy) {
        return proxy;
    }

    public DiService beforeCreate(DiService delegate, Constructor constructor, Object[] args) {
        return delegate;
    }

    protected ConnTransAopContext getContext() {
        ConnTransAopContext ctx = threadLocal.get();
        if (ctx == null) {
            ctx = new ConnTransAopContext();
            threadLocal.set(ctx);
        }
        return ctx;
    }
    
    public void before(DiService delegate, Method method, Object[] args) {
        this.beforeFinished = false;
        log.debug("Before weaving of {}.", delegate);
        
        
        ConnTransAopContext ctx = getContext();
        
        this.beforeCascadeCnt = ctx.getCascade();
        this.beforeConnAllocCnt = ctx.getConnAllocCnt();
        this.connAlloced = ctx.getConn() != null;
        
        DbConnection conn = ctx.getConn();
        if (conn != null) {
            log.debug("Delegate {} with ConnTransAopContext {} of threadLocal {}, found conn {} of .", delegate, ctx, threadLocal, conn);
        } else {
            conn = allocConn(delegate, method);
            beginTrans(conn);
            log.debug("Delegate {} with ConnTransAopContext {} of threadLocal {}, no exists conn, initialize conn {} .", delegate, ctx, threadLocal, conn);
        }
        delegate.setConn(conn);

        ctx.increaseCascade();
        
        this.beforeFinished = true;
        
    }
    
    public Object after(DiService delegate, Method method, Object[] args, Object result) {
        log.debug("After weaving of {}.", delegate);
        
        ConnTransAopContext ctx = getContext();
        DbConnection conn = ctx.getConn();
        log.debug("Commit conn {} of {} .", conn, delegate);
        commit(conn);
        releaseConn(delegate, method);
        delegate.setConn(null);
        ctx.decreaseCascade();
        checkStatus(delegate, method, args);
        return result;
    }

    public void error(DiService delegate, Method method, Object[] args, Exception ex) {
        log.debug("Error weaving of {} for {}.", delegate, ex);
        
        ConnTransAopContext ctx = getContext();
        DbConnection conn = ctx.getConn();
        try {
            rollback(conn);
        } catch (Throwable e) {
            log.error("When rollback conn {}, Exception occured {}", conn, e);
        } finally {
            if (this.initConn && !this.releasedConn) {
                log.debug("Release conn {} of {} for exception {}.", conn, delegate, ex);
                try {
                    releaseConn(delegate, method);
                } catch (DiException e) {
                    log.error("Release conn {} failed for {} .", conn, e);
                }
            }
            if (this.beforeFinished) {
                ctx.decreaseCascade();
            }
            checkStatus(delegate, method, args);
        }
        throw new DiException(ex);
    }
    
    public void finalli(DiService delegate, Method method, Object[] args) {
        //checkStatus(delegate, method, args);
    }
    
    public void checkStatus(DiService delegate, Method method, Object[] args) {
        log.debug("Finally weaving of {}.", delegate);
        
        ConnTransAopContext ctx = getContext();
        int afterCascadeCnt = ctx.getCascade();
        if (this.beforeCascadeCnt != afterCascadeCnt) {
            log.error("Aop {} of delegate {} method {} before weaving cascade count {} not equals finally {}.", this, delegate, method, this.beforeCascadeCnt, afterCascadeCnt);
        }
        int afterConnAllocCnt = ctx.getConnAllocCnt();
        if (this.beforeConnAllocCnt != afterConnAllocCnt) {
            log.error("Aop {} of delegate {} method {} before weaving conn alloc count {} not equals finally {}.", this, delegate, method, this.beforeConnAllocCnt, afterConnAllocCnt);
        }
        if (this.connAlloced != (ctx.getConn() != null)) {
            log.error("Aop {} of delegate {} method {} before weaving conn alloc status {} not equals finally {}.", this, delegate, method, this.connAlloced, (ctx.getConn() != null));
        }
        if (this.initConn && !this.releasedConn) {
            log.error("Aop {} of delegate {} method {} conn allocated but not released.", this, delegate, method);
        }
        if (this.initTx && !this.endedTx) {
            log.error("Aop {} of delegate {} method {} transaction started but not committed or rollbacked.", this, delegate, method);
        }
        
    }

    
    private void beginTrans (DbConnection conn) {
        if (!conn.getPool().isAutoCommit()) {
            if (this.initTx) {
                log.error("Cannot overwrite initTx true.");
                throw new DiException ("Cannot overwrite initTx true.");
            } else {
                this.initTx = true;
            }
        }
    }
    
    private void commit (DbConnection conn) {
        try {
            if (this.initTx && !this.endedTx &&
                    !conn.getPool().isAutoCommit()) {
                conn.getConn().commit();
                this.endedTx = true;
            }
        } catch (SQLException e) {
            throw new DiException(e);
        }
    }
    
    private void rollback (DbConnection conn) {
        if (this.initTx && !this.endedTx &&
                !conn.getPool().isAutoCommit()) {
            if (conn == null) {
                log.error("Get null connection from context {} when try to rollback it.", getContext());
            } else {
                if (conn.getConn() == null) {
                    log.error("Get null JDBC Connection from DbConnection {} when try to rollback it.", conn);
                } else {
                    try {
                        conn.getConn().rollback();
                        this.endedTx = true;
                    } catch (SQLException e) {
                        log.error("Error occured when rollback conn : ", e);
                        throw new DiException(e);
                    }
                }
            }
        }
    }
    
    private DbConnection allocConn(DiService delegate, Method method) {
        ConnTransAopContext ctx = getContext();
        DbConnection conn;
        // not necessary synchronized for delegate and context
        if ((conn = ctx.getConn()) != null || (this.initConn && !this.releasedConn)) {
            log.error("Cannot allocConn overwrite exists conn {} or initConn {} with releasedConn {} for Delegate {} with ConnTransAopContext {} of threadLocal {}.", conn, this.initConn, this.releasedConn, delegate, ctx, threadLocal);
            throw new DiException ("Cannot allocConn overwrite exists conn " + conn + " initConn " + this.initConn + " releasedConn " + this.releasedConn + ".");
        } else {
            conn = container.get(ConnPool.class).get();
            conn.getDiagDict().put(this.getClass().getSimpleName(), this);
            conn.getDiagDict().put(Thread.class.getSimpleName(), Thread.currentThread());
            ctx.setConn(conn);
            this.initConn = true;
            this.releasedConn = false;
            ctx.increaseConnAllocCnt();
        }
        return conn;
    }
    
    private void releaseConn(DiService delegate, Method method) {
        ConnTransAopContext ctx = getContext();
        if (this.initConn && !this.releasedConn) {
            DbConnection conn = ctx.getConn();
            if (conn == null) {
                log.error("Get null connection from context when try to release it.");
                throw new DiException("Get null connection from context when try to release it.");
            } else {
                log.debug("Release conn {} of {} .", conn, delegate);
                conn.getDiagDict().remove(this.getClass().getSimpleName());
                conn.getDiagDict().remove(Thread.class.getSimpleName());
                container.get(ConnPool.class).release(conn);
                ctx.setConn(null);
                ctx.decreaseConnAllocCnt();
                this.releasedConn = true;
            }
        }
    }

}
